﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

using $safeprojectname$.Context;

namespace $projectname$.Repositories.Base
{
    /// <summary>
    /// Abstract base class for a generic repository.
    /// </summary>
    /// <typeparam name="T">Type of entity the repository will interact with.</typeparam>
    /// <remarks>
    /// Refer http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application.
    /// Refer http://blog.magnusmontin.net/2013/05/30/generic-dal-using-entity-framework/.
    /// </remarks>
    public abstract class GenericDataRepository<T> : IGenericDataRepository<T>, IDisposable where T : class
    {

        #region Constructors

        /// <summary>
        /// Initialises a new instance of the <see cref="GenericDataRepository{T}" /> class.
        /// </summary>
        /// <param name="userContext">End-user context within which the repository is being accessed.</param>
        /// <param name="autoSave">Indicates whether to automatically save changes as part of action methods (Add, Modify, Remove). Defaults to true.</param>
        protected GenericDataRepository(bool autoSave = true)
            : this(autoSave, new DatabaseContext())
        {

        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether to automatically save changes as part of action methods (Add, Modify, Remove).
        /// </summary>
        public bool AutoSave { get; set; }
        
        /// <summary>
        /// Gets or sets the DirectoryDbContext the repository is using as a persistence store.
        /// </summary>
        private DatabaseContext DbContext { get; set; }

        /// <summary>
        /// Gets or sets the DbSet containing the entities the repository is being used to interact with.
        /// </summary>
        protected DbSet<T> DbSet { get; set; }

        #endregion

        #region Methods

        /// <summary>
        /// Returns a list of items from the repository matching the provided criteria.
        /// </summary>
        /// <param name="filter">Filter criteria.</param>
        /// <param name="navigationProperties">Additional navigation properties to be included in the results.</param>
        /// <returns>List of matching items.</returns>
        public virtual List<T> List(Expression<Func<T, bool>> filter = null, params Expression<Func<T, object>>[] navigationProperties)
        {
            // Set up query.
            IQueryable<T> query = this.DbSet;
            query.AsNoTracking();

            // Add filter to query if specified.
            if (filter != null)
            {
                query = query.Where(filter);
            }

            // Add eager loading of navigation properties if specified.
            if (navigationProperties != null)
            {
                foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                {
                    query = query.Include<T, object>(navigationProperty);
                }
            }

            // Return query results.
            return query.ToList<T>();
        }

        /// <summary>
        /// Returns a batch of items from the repository. Among those which
        /// meet the filter criteria, it will take a maximum of count items
        /// after ordering by the keySelector.
        /// </summary>
        /// <param name="filter">Filter criteria.</param>
        /// <param name="keySelector">Determines the ordering of elements and therefore which ones are included in the batch.</param>
        /// <param name="count">Determines how many elements should be returned.</param>
        /// <param name="navigationProperties">Additional navigation properties to be included in the results.</param>
        /// <returns>List of matching items.</returns>
        public virtual List<T> Batch(Expression<Func<T, bool>> filter, Expression<Func<T, int>> keySelector, int count, params Expression<Func<T, object>>[] navigationProperties)
        {
            // Set up query.
            IQueryable<T> query = this.DbSet;
            ////query = query.AsNoTracking();
            query = query.Where(filter);
            query = query.OrderBy(keySelector);
            query = query.Take(count);

            // Add eager loading of navigation properties if specified.
            if (navigationProperties != null)
            {
                foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                {
                    query = query.Include<T, object>(navigationProperty);
                }
            }

            // Return query results.
            return query.ToList<T>();
        }

        /// <summary>
        /// Returns a single item from the repository matching the provided criteria.
        /// </summary>
        /// <param name="filter">Filter criteria</param>
        /// <param name="navigationProperties">Additional navigation properties to be included in the results.</param>
        /// <returns>Matching item, or null if no items match.</returns>
        public virtual T Get(Expression<Func<T, bool>> filter, params Expression<Func<T, object>>[] navigationProperties)
        {
            // Set up query.
            IQueryable<T> query = this.DbSet;
            query.AsNoTracking();

            // Add eager loading of navigation properties if specified.
            if (navigationProperties != null)
            {
                foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                {
                    query = query.Include<T, object>(navigationProperty);
                }
            }

            // Return query results.
            return query.SingleOrDefault<T>(filter);
        }

        /// <summary>
        /// Adds an item to the repository.
        /// </summary>
        /// <param name="item">Item to be added.</param>
        /// <remarks>
        /// If the AutoSave property is true, adds the item and saves it automatically.
        /// If the AutoSave property is false, adds the item and waits for the Save method to be called explicitly.
        /// </remarks>
        public virtual void Add(T item)
        {
            this.DbSet.Add(item);
            this.DbContext.Entry(item).State = EntityState.Added;
            if (this.AutoSave)
            {
                this.Save();
            }
        }

        /// <summary>
        /// Modifies an item in the repository.
        /// </summary>
        /// <param name="item">Modified item.</param>
        /// <remarks>
        /// If the AutoSave property is true, modifies the item and saves it automatically.
        /// If the AutoSave property is false, modifies the item and waits for the Save method to be called explicitly.
        /// </remarks>
        public virtual void Modify(T item)
        {
            this.DbSet.Attach(item);
            this.DbContext.Entry(item).State = EntityState.Modified;
            if (this.AutoSave)
            {
                this.Save();
            }
        }

        /// <summary>
        /// Removes an item from the repository.
        /// </summary>
        /// <param name="item">Item to be removed.</param>
        /// <remarks>
        /// If the AutoSave property is true, removes the item and saves the removal automatically.
        /// If the AutoSave property is false, removes the item and waits for the Save method to be called explicitly.
        /// </remarks>
        public virtual void Remove(T item)
        {
            if (this.DbContext.Entry(item).State == EntityState.Detached)
            {
                this.DbSet.Attach(item);
            }
            this.DbSet.Remove(item);
            if (this.AutoSave)
            {
                this.Save();
            }
        }

        /// <summary>
        /// Saves changes from the repository to the underlying persistence store.
        /// </summary>
        public virtual void Save()
        {
            this.DbContext.SaveChanges();
        }

        /// <summary>
        /// Dispose resources.
        /// </summary>
        public void Dispose()
        {
            if (this.DbContext != null)
            {
                this.DbContext.Dispose();
                this.DbContext = null;
            }
        }

        #endregion

    }

}

